你今天註冊了嗎?用後端好朋友 express 跟 middleware 們做一個簡單的會員註冊系統


Posted by Christy on 2022-01-11

本文為 Lidemy [BE201] > [超重要觀念:Middleware] 之 [做一個簡單會員註冊系統] 來學習後端 express 的實作過程,以下摘要內含:實作目標、資料夾結構、基本流程、MVC 功能簡介、使用 bcrypt middleware 加密密碼

零、內容摘要

1. 目標是要做出三個頁面,分別是首頁、註冊及登入,可以寫入且讀取資料庫

2. 資料夾結構

  • controllers

    • user.js: 一個個函式
  • models

    • user.js:跟之前寫 sql query 類似
  • views

    • user folder

      • login.ejs:登入頁面的畫面

      • register.ejs:註冊頁面的畫面

    • index.ejs:畫面的首頁

  • db.js:帳號、密碼,負責連線

  • index.js:負責引入 library 及路由

3. 基本流程:開 table → 寫 models → 寫 views → index.js 寫路由 → 寫 controllers

註:每寫一小段就跑一下看看

4. models: 以前 sql query 的寫法,select or insert into…

5. views: html tag + <%= js 想輸出的東西 %>

6. controllers: 包著一個個函式:

a. 讀取的(.get())就只渲染畫面

login: (req, res) => {
  res.render('user/login')
},

b. 寫入的(.post())裡面拿資料並且處理錯誤

handleLogin: (req, res, next) => {
  const { username, password, nickname } = req.body
  if (!username || !password) {
    req.flash('errorMessage', 'Please fill in all the required fields.')
    return next()
  }
  userModel.get(username, (err, user) => {
    if (err) {
      req.flash('errorMessage', err.toString())
      return next()
    }
    if (!user) {
      req.flash('errorMessage', 'invalid user')
      return next()
    }
    bcrypt.compare(password, user.password, function(err, isSuccess) {
      if (err || !isSuccess) {
        req.flash('errorMessage', 'invalid password')
        return next()
      }
      req.session.username = user.username
      res.redirect('/')
    });
  })
},handleLogin: (req, res, next) => {
  const { username, password, nickname } = req.body
  if (!username || !password) {
    req.flash('errorMessage', 'Please fill in all the required fields.')
    return next()
  }
  userModel.get(username, (err, user) => {
    if (err) {
      req.flash('errorMessage', err.toString())
      return next()
    }
    if (!user) {
      req.flash('errorMessage', 'invalid user')
      return next()
    }
    bcrypt.compare(password, user.password, function(err, isSuccess) {
      if (err || !isSuccess) {
        req.flash('errorMessage', 'invalid password')
        return next()
      }
      req.session.username = user.username
      res.redirect('/')
    });
  })
},

7. 用了 bcrypt 這個 middleware 來加密密碼

一、基本設置

1. sequel pro 開一個叫 users 的 table,包含:

id, username, password, nickname

2. 先寫 model

models > user.js

const db = require('../db')

const userModel = {
  // 傳一個 user 裡面包含所有資料
  add: (user, cb) => {
    db.query(
      'insert into users(username, password, nickname) values(?, ?, ?)',
        [user.username, user.password, user.nickname],
      (err, results) => {
        if (err) return cb(err)
        cb(null)
      });
  },

  get: (username, cb) => {
    db.query(
      'SELECT * from users where username = ?', [username],
      (err, results) => {
        if (err) return cb(err)
        cb(null, results[0])
      });
  }
}

module.exports = userModel

3. 把剛剛 index.js 裡面的 controller 整理一下

// index.js

app.get('/login', userController.login)
app.post('/login', userController.handleLogin)
app.get('/logout', userController.logout)

4. 把剛剛 index.js 裡面的函式放過來 controllers > user

// controllers > user.js

const userModel = require('../models/user')

const userController = {
  get: (req, res) => {

  },

  login: (req, res) => {
    res.render('login')
  },

  handleLogin: (req, res) => {
    if (req.body.password === 'abc') {
      req.session.isLogin = true
      res.redirect('/')
    } else {
      req.flash('errorMessage', 'invalid password')
      res.redirect('/login')
    }
  },

  logout: (req, res) => {
    req.session.isLogin = false
    res.redirect('/')
  }
}

module.exports = userController

5. views > user.js

// index.ejs

<h1>簡易會員系統</h1>

<% if(isLogin) { %>
  Hello, user!
  <a href="logout">logout</a>
<% } else { %>
  Wanna login?
  <a href="/register">register</a>
  <a href="/login">login</a>
<% } %>

要記得在 index.js 渲染畫面才會出現喔

// index.js

app.get('/', (req, res) => {
  res.render('index')
})

二、實作註冊頁面

1. index.js 加上路由

// index.js

app.get('/register', userController.register)
app.post('/register', userController.handleRegister)

2. controllers

error log 1: Error: Route.post() requires a callback function but got a [object Undefined]

錯誤描述:localhost:5001 無法跑起來

解法一:先把這行註解 app.post('/register', userController.handleRegister) ,估計是因為 controller 裡面的 handleRegister 沒有寫好

後來發現是拼錯字啦,吼唷

error log 2: Error: Connection lost: The server closed the connection.

[Nodejs] 解決MySQL Error: Connection lost. The server closed the connection的方法:這個還沒解決,之後再嘗試吧

// controller > user.js

const res = require('express/lib/response')
const userModel = require('../models/user')

const userController = {
  get: (req, res) => {

  },

  login: (req, res) => {
    res.render('login')
  },

  handleLogin: (req, res) => {
    if (req.body.password === 'abc') {
      req.session.isLogin = true
      res.redirect('/')
    } else {
      req.flash('errorMessage', 'invalid password')
      res.redirect('/login')
    }
  },

  register: (req, res) => {
    res.render('user/register')
  },

  handleRegister: (req, res) => {
    const { username, password, nickname } = req.body
    if (!username || !password || !nickname) {
      return req.flash('errorMessage', 'Please fill in all the required fields.')
    }
    bcrypt.hash(myPlaintextPassword, saltRounds, function(err, hash) {
        // Store hash in your password DB.
    });
    userModel.add({
      username,
      password,
      nickname
    }, (err) => {
      if (err) {
        return req.flash('errorMessage', err.toString())
      }
      req.session.username = username
      res.redirect('/')
    })
  },

  logout: (req, res) => {
    req.session.username = null
    res.redirect('/')
  }
}

module.exports = userController

3. views 註冊頁面

// views > register.ejs

<h1>register</h1>

<h2><%= errorMessage %></h2>
<form method="POST" action="/register">
  <div>username: <input type="text" name="username" /></div>
  <div>nickname: <input type="text" name="nickname" /></div>
  <div>password: <input type="password" name="password" /></div>
  <input type="submit" />
</form>

4. 密碼加密

a. 參考 node.bcrypt.js 安裝 $ npm install bcrypt

b. 注意這裡要在 controllers > user.js 裡面引入

// controllers > user.js

const bcrypt = require('bcrypt')
const saltRounds = 10

c. 下面程式碼放在 controller > user.js 的 handleRegister 拿到密碼之後

bcrypt.hash(myPlaintextPassword, saltRounds, function(err, hash) {
    // Store hash in your password DB.
});
handleRegister: (req, res) => {
  const { username, password, nickname } = req.body
  if (!username || !password || !nickname) {
    return req.flash('errorMessage', 'Please fill in all the required fields.')
  }

  bcrypt.hash(password, saltRounds, function(err, hash) {
    if (err) {
      return req.flash('errorMessage', err.toString())
    }
    userModel.add({
      username,
      password: hash,
      nickname
    }, (err) => {
      if (err) {
        return req.flash('errorMessage', err.toString())
      }
      req.session.username = username
      res.redirect('/')
    })
  });
},

三、實作登入頁面

1. 從路由開始

// index.js

app.get('/login', userController.login)
app.post('/login', userController.handleLogin, redirectBack)
app.get('/logout', userController.logout)

2. controllers 就會呼叫相對應的 models

// controllers > user.js

const userModel = require('../models/user')
const bcrypt = require('bcrypt')
const saltRounds = 10

const userController = {
  login: (req, res) => {
    res.render('login')
  },

  handleLogin: (req, res, next) => {
    const { username, password, nickname } = req.body
    if (!username || !password) {
      req.flash('errorMessage', 'Please fill in all the required fields.')
      return next()
    }
    userModel.get(username, (err, user) => {
      if (err) {
        req.flash('errorMessage', err.toString())
        return next()
      }
      if (!user) {
        req.flash('errorMessage', 'invalid user')
        return next()
      }
      bcrypt.compare(password, user.password, function(err, isSuccess) {
        if (err || !isSuccess) {
          req.flash('errorMessage', 'invalid password')
          return next()
        }
        req.session.username = user.username
        res.redirect('/')
      });
    })
  },

  register: (req, res) => {
    res.render('user/register')
  },

  handleRegister: (req, res, next) => {
    const { username, password, nickname } = req.body
    if (!username || !password || !nickname) {
      req.flash('errorMessage', 'Please fill in all the required fields.')
      return next()
    }

    bcrypt.hash(password, saltRounds, function(err, hash) {
      if (err) {
        req.flash('errorMessage', 'user exists')
        return next()
      }
      userModel.add({
        username,
        password: hash,
        nickname
      }, (err) => {
        if (err) {
          req.flash('errorMessage', 'user exists')
          return next()
        }
        req.session.username = username
        res.redirect('/')
      })
    });
  },

  logout: (req, res) => {
    req.session.username = null
    res.redirect('/')
  }
}

module.exports = userController

3. model 會是這樣

// models > user.js

const db = require('../db')

const userModel = {
  add: (user, cb) => {
    db.query(
      'insert into users(username, password, nickname) values(?, ?, ?)',
      [user.username, user.password, user.nickname],
      (err, results) => {
        if (err) return cb(err)
        cb(null)
      }
    );
  },

  get: (username, cb) => {
    db.query(
      'SELECT * from users where username = ?', [username],
      (err, results) => {
        if (err) return cb(err)
        cb(null, results[0])
      }
    );
  }
}

module.exports = userModel

4. template engine 會自動防止 xss、sql injection 攻擊

  • xss:記得要用的是 <%= username %>,如果是減號 <%- username %> 就不會防止攻擊,輸入什麼就是什麼

  • sql injection:用 ? 代替輸入,'SELECT * from users where username = ?', [username],










Related Posts

Git筆記 - in practice

Git筆記 - in practice

打造後台管理系統的好幫手:Ant Design

打造後台管理系統的好幫手:Ant Design

JavaScript未分類

JavaScript未分類


Comments